package com.etymgiko.synccommandexecutor;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Timer;
import java.util.TimerTask;

/**
 * Allows synchronous execution of commands. That is command is started and current thread waits
 * until it finishes or maximum wait time is elapsed. If command is being executed more than defined
 * maximum wait time then execution of this command is terminated. To ensure that command is executing
 * SyncCommandExecutor handles output and error streams of this command. SyncCommandExecutor is suitable
 * for execution of bat files that generate a lot of output.
 * @author Ivan Holub
 */
public class SyncCommandExecutor {

    /**
     * Log.
     */
    private Log log = LogFactory.getLog(getClass());

    /**
     * Number of milliseconds in one second.
     */
    private static final int SECOND = 1000;

    /**
     * Name of thread "errorReader" to display in log.
     */
    private static final String THREAD_ERR = "thread1";

    /**
     * Name of thread "inputReader" to display in log.
     */
    private static final String THREAD_OUT = "thread2";

    /**
     * Name of thread "waiter" to display in log.
     */
    private static final String THREAD_WAITER = "thread3";

    /**
     * Name of thread "timer" to display in log.
     */
    private static final String THREAD_TIMER = "thread4";

    /**
     * Thread that reads error stream of process.
     */
    private Thread errorReader;

    /**
     * Thread that reads input stream of process.
     */
    private Thread inputReader;

    /**
     * Thread that waits until executing of process is finished.
     */
    private Thread waiter;

    /**
     * Mutex to synchronize access to log.
     */
    private final Object mutexLog = new Object();

    /**
     * Mutex is used to tell that it necessary to finish executing command.
     */
    private final Object mutexEndJob = new Object();

    /**
     * Mutex to synchronize saving information about occured error.
     */
    private final Object mutexSaveError = new Object();

    /**
     * Command to execute.
     */
    private String command;

    /**
     * Process in which command is executed.
     */
    private Process process;

    /**
     * Maximum wait time in seconds.
     */
    private int waitTime; // seconds

    /**
     * Timer that waits defined maximum wait time.
     */
    private Timer timer;

    /**
     * Task for timer.
     */
    private TimerTask timerTask;

    /**
     * Either error occured.
     */
    private volatile boolean isErrorOccured;

    /**
     * Either process is being executed more then defined maximum wait time.
     */
    private volatile boolean isTimeOut;

    /**
     * Occured error.
     */
    private Exception error;

    /**
     * Task for timer.
     */
    private class SyncTimerTask extends TimerTask {

        private Runnable task;

        public SyncTimerTask(final Runnable task) {
            this.task = task;
        }

        public void run() {
            task.run();
        }

    }

    /**
     * Creates command executor.
     *
     * @param command  the command to execute
     * @param waitTime maximum wait time
     */
    public SyncCommandExecutor(final String command, final int waitTime) {
        super();
        this.command = command;
        this.waitTime = waitTime;
    }

    /**
     * Starts executing command.
     */
    public final void runProcess() {
        log("run process, command: " + command + ", wait time: " + waitTime);
        try {
            process = Runtime.getRuntime().exec(command);
        } catch (IOException e) {
            log("Error", e);
            saveError(e);
        }

        if (!isErrorOccured) {
            waitProcess();
        }
    }

    /**
     * Waits until executing of command is finished or maximum time is elapsed.
     */
    private void waitProcess() {
        log("wait process");

        //read error stream of process
        errorReader = new Thread(new Runnable() {

            public void run() {
                final String msgStart = THREAD_ERR + ": ";
                log(msgStart + "start");
                try {
                    InputStreamReader isr = new InputStreamReader(process
                            .getErrorStream());
                    BufferedReader br = new BufferedReader(isr);
                    String line;
                    while ((line = br.readLine()) != null) {
                        log(msgStart + line);
                    }

                } catch (IOException ioe) {
                    saveError(ioe);
                    log(msgStart + "Error", ioe);
                    notifyJobFinish();
                }
                log(msgStart + "end");
            }

        });

        //read input stream of process
        inputReader = new Thread(new Runnable() {

            public void run() {
                final String msgStart = THREAD_OUT + ": ";
                log(msgStart + "start");
                try {
                    InputStreamReader isr = new InputStreamReader(process
                            .getInputStream());
                    BufferedReader br = new BufferedReader(isr);
                    String line;
                    while ((line = br.readLine()) != null) {
                        log(msgStart + line);
                    }
                } catch (IOException ioe) {
                    saveError(ioe);
                    log(msgStart + "Error", ioe);
                    notifyJobFinish();
                }
                log(msgStart + "end");
            }

        });

        //wait until executing of process is finished
        waiter = new Thread(new Runnable() {

            public void run() {
                final String msgStart = THREAD_WAITER + ": ";
                log(msgStart + "start");
                try {
                    int exitValue = process.waitFor();
                    log(msgStart + "exitValue = " + exitValue);

                    notifyJobFinish();

                } catch (InterruptedException e) {
                    if (isTimeOut) {
                        log(msgStart + "process interrupted");
                    } else {
                        saveError(e);
                        log(msgStart + "Error", e);
                        notifyJobFinish();
                    }
                }
                log(msgStart + "end");
            }

        });

        //is called if command is being executed more then defined maximum wait time
        Runnable timerTaskDoBlock = new Runnable() {
            public void run() {
                final String msgStart = THREAD_TIMER + ": ";
                log(msgStart + "start");
                isTimeOut = true;

                process.destroy();

                log(msgStart + "time out");
                notifyJobFinish();
                log(msgStart + "end");
            }
        };

        timer = new Timer();
        timerTask = new SyncTimerTask(timerTaskDoBlock);

        errorReader.start();
        inputReader.start();
        waiter.start();

        //wait defined maximum wait time
        timer.schedule(timerTask, waitTime * SECOND);

        waitFinish();
        killAllThreads();
    }

    /**
     * Tells that it necessary to finish executing command.
     */
    private void notifyJobFinish() {
        synchronized (mutexEndJob) {
            log("notify job finish");
            timerTask.cancel();
            mutexEndJob.notify();
        }

    }

    /**
     * Waits until executing of process is finished or command is being executed
     * more than defined maximum wait time.
     */
    private void waitFinish() {

        synchronized (mutexEndJob) {
            try {
                log("waiting start");
                mutexEndJob.wait();
                log("waiting end");
            } catch (InterruptedException e) {
                saveError(e);
                log("Error", e);
            }
        }

    }

    /**
     * Kills threads.
     */
    private void killAllThreads() {
        log("finishing all threads start");
        if (errorReader.isAlive()) {
            log("interrupt thread: " + THREAD_ERR);
            errorReader.interrupt();
        }
        if (inputReader.isAlive()) {
            log("interrupt thread: " + THREAD_OUT);
            inputReader.interrupt();
        }
        if (waiter.isAlive()) {
            log("interrupt thread: " + THREAD_WAITER);
            waiter.interrupt();
        }
        log("finishing all threads end");
    }

    /**
     * Write message to log.
     *
     * @param message the message
     */
    private void log(final String message) {
        synchronized (mutexLog) {
            log.debug(message);
        }
    }

    /**
     * Write error to log.
     *
     * @param message the message
     * @param ex      the exception
     */
    private void log(final String message, final Exception ex) {
        synchronized (mutexLog) {
            log.error(message, ex);
        }
    }

    /**
     * Either error occured.
     *
     * @return true if error occured
     */
    public final boolean isErrorOccured() {
        return isErrorOccured;
    }

    /**
     * Either process is being executed more then defined maximum wait time.
     *
     * @return true if process is being executed more then defined maximum wait time
     */
    public final boolean isTimeOut() {
        return isTimeOut;
    }

    /**
     * Store occured error.
     *
     * @param anError the exception
     */
    private void saveError(final Exception anError) {
        synchronized (mutexSaveError) {
            if (!isErrorOccured) {
                isErrorOccured = true;
                error = anError;
            }
        }
    }

    /**
     * Gets occured error.
     *
     * @return occured error
     */
    public final Exception getError() {
        return error;
    }

}
